package cn.bossfriday.im.common;

import cn.bossfriday.common.common.SystemConfig;
import cn.bossfriday.common.common.SystemConstant;
import cn.bossfriday.common.exception.ServiceRuntimeException;
import cn.bossfriday.common.http.HttpProcessorMapper;
import cn.bossfriday.common.http.IHttpProcessor;
import cn.bossfriday.common.plugin.IPlugin;
import cn.bossfriday.common.plugin.PluginApplication;
import cn.bossfriday.common.register.ActorRegister;
import cn.bossfriday.common.register.ActorRoute;
import cn.bossfriday.common.register.HttpApiRoute;
import cn.bossfriday.common.router.ClusterRouterFactory;
import cn.bossfriday.common.rpc.actor.BaseUntypedActor;
import cn.bossfriday.common.utils.ClassLoaderUtil;
import cn.bossfriday.common.utils.CommonUtils;
import cn.bossfriday.common.utils.ThreadPoolUtil;
import cn.bossfriday.im.common.db.DbUtilExecutor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.reflections.Reflections;

import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * PluginBootstrap
 *
 * @author chenx
 */
@Slf4j
public abstract class PluginBootstrap implements IPlugin {

    private static final int DEFAULT_MIN;
    private static final int DEFAULT_MAX;

    static {
        DEFAULT_MIN = (SystemConstant.CPU_PROCESSORS / 2) <= 0 ? 1 : (SystemConstant.CPU_PROCESSORS / 2);
        DEFAULT_MAX = SystemConstant.CPU_PROCESSORS;
    }

    /**
     * start
     */
    protected abstract void start();

    /**
     * stop
     */
    protected abstract void stop();

    @Override
    public void startup(SystemConfig config) {
        try {
            long begin = System.currentTimeMillis();
            if (config == null) {
                throw new ServiceRuntimeException("ServiceConfig is null");
            }

            PluginApplication pluginAnno = getPluginApplication(this.getClass());
            String serviceName = pluginAnno.name();

            // 1.Check DB Connection
            this.checkDbConnection();

            // 2.Register ActorRoute & HttpApiRoute
            ClusterRouterFactory.build(config);
            this.registerService(config, serviceName);
            ClusterRouterFactory.getClusterRouter().registryService();
            ClusterRouterFactory.getClusterRouter().startActorSystem();

            // 3.Start Plugin
            this.start();
            long time = System.currentTimeMillis() - begin;
            String logInfo = "[" + serviceName + "] Start Done, RpcPort:" + config.getClusterNode().getPort() + ", Time:" + time;
            CommonUtils.printSeparatedLog(log, logInfo);
        } catch (InterruptedException interEx) {
            log.error("ServicePluginBootstrap.startup() InterruptedException!", interEx);
            Thread.currentThread().interrupt();
        } catch (Exception ex) {
            log.error("ServicePluginBootstrap.startup() error!", ex);
        }
    }

    @Override
    public void shutdown() {
        try {
            this.stop();
        } catch (Exception ex) {
            log.error("ServicePluginBootstrap.shutdown() error!", ex);
        }
    }

    /**
     * checkDbConnection
     */
    private void checkDbConnection() throws SQLException {
        if (!DbUtilExecutor.checkConnection()) {
            throw new ServiceRuntimeException("DB Connection Test Failed!");
        }

        log.info("checkDbConnection done: " + DbUtilExecutor.getDataSource().getJdbcUrl());
    }


    /**
     * registerService
     */
    @SuppressWarnings("squid:S3776")
    private void registerService(SystemConfig config, String serviceName) throws IOException, ClassNotFoundException {
        List<Class<? extends BaseUntypedActor>> actorClassList = new ArrayList<>();
        List<Class<? extends IHttpProcessor>> httpProcessorList = new ArrayList<>();

        // 优先配置中的显示声明，无配置反射当前jar包；
        if (!CollectionUtils.isEmpty(config.getPluginJarFilePath())) {
            for (String jarFilePath : config.getPluginJarFilePath()) {
                if (StringUtils.isNotEmpty(jarFilePath)) {
                    File file = new File(jarFilePath);
                    if (!file.exists()) {
                        log.warn("PluginJarFile not existed! path={}", jarFilePath);
                        continue;
                    }

                    actorClassList.addAll(ClassLoaderUtil.getAllClass(jarFilePath, BaseUntypedActor.class));
                    httpProcessorList.addAll(ClassLoaderUtil.getAllClass(jarFilePath, IHttpProcessor.class));

                }
            }
        } else {
            actorClassList.addAll(new Reflections(this.getClass().getPackage().getName()).getSubTypesOf(BaseUntypedActor.class));
            httpProcessorList.addAll(new Reflections(this.getClass().getPackage().getName()).getSubTypesOf(IHttpProcessor.class));
        }

        // register actor
        if (!CollectionUtils.isEmpty(actorClassList)) {
            actorClassList.forEach(cls -> {
                if (cls.isAnnotationPresent(ActorRoute.class)) {
                    ActorRoute route = cls.getAnnotation(ActorRoute.class);
                    this.registerActorRoute(cls, route, serviceName);
                }
            });
        }

        // load http processor
        if (!CollectionUtils.isEmpty(httpProcessorList)) {
            httpProcessorList.forEach(cls -> {
                if (cls.isAnnotationPresent(HttpApiRoute.class)) {
                    HttpApiRoute route = cls.getAnnotation(HttpApiRoute.class);
                    Class<? extends IHttpProcessor> entry = HttpProcessorMapper.putHttpProcessor(route.apiRouteKey(), cls);
                    if (Objects.isNull(entry)) {
                        log.info("[HttpProcessor] - {}", cls.getName());
                    }
                }
            });
        }
    }

    /**
     * registerActorRoute
     *
     * @param cls
     * @param route
     */
    private void registerActorRoute(Class<? extends BaseUntypedActor> cls, ActorRoute route, String serviceName) {
        if (ArrayUtils.isEmpty(route.methods())) {
            return;
        }

        String poolName = StringUtils.isEmpty(route.poolName()) ? "Pool#" + serviceName : route.poolName();
        int poolSize = route.poolSize() > 0 ? route.poolSize() : ThreadPoolUtil.AVAILABLE_PROCESSORS * 2;
        for (String method : route.methods()) {
            try {
                ActorRegister.registerActor(method, cls, getActorExecutorMin(route), getActorExecutorMax(route), poolName, poolSize);
                log.info("[Actor] - {}, {}, {}", cls.getName(), poolName, poolSize);
            } catch (Exception ex) {
                log.error("registerActor error!", ex);
            }
        }
    }

    /**
     * getActorExecutorMin
     *
     * @param route
     * @return
     */
    private static int getActorExecutorMin(ActorRoute route) {
        if (route.min() > DEFAULT_MIN) {
            return route.min();
        }

        return DEFAULT_MIN;
    }

    /**
     * getActorExecutorMax
     *
     * @param route
     * @return
     */
    private static int getActorExecutorMax(ActorRoute route) {
        if (route.max() > DEFAULT_MAX) {
            return route.max();
        }

        return DEFAULT_MAX;
    }

    /**
     * getPluginApplication
     *
     * @param clazz
     * @return
     */
    private static PluginApplication getPluginApplication(Class<?> clazz) {
        if (clazz.isAnnotationPresent(PluginApplication.class)) {
            return clazz.getAnnotation(PluginApplication.class);
        }

        throw new IllegalStateException("Annotation @PluginApplication not found on class: " + clazz.getName());
    }
}
